fix(csp): remove unsafe-inline from script-src and harden security headers#876
fix(csp): remove unsafe-inline from script-src and harden security headers#876advikdivekar wants to merge 6 commits into
Conversation
|
@advikdivekar is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel. A member of the Team first needs to authorize it. |
GSSoC Label Checklist 🏷️@Priyanshu-byte-coder — please apply the appropriate labels before merging: Difficulty (pick one):
Quality (optional):
Validation (required to score):
|
|
Great work on this PR — the CSP hardening and token exposure fix are both important security improvements. Before merging, there are 6 more API routes that still use
Please migrate these to use |
|
This PR has merge conflicts with |
yes working on it, almost done, raising PR in few mins |
601c978 to
9f8b7bb
Compare
Migrate all metrics API routes to retrieve the GitHub OAuth token server-side via getGitHubAccessToken(req) instead of reading the now-removed session.accessToken field. Covers contributions, prs, pr-review-time, streak, repos, repo-health, ci, and compare routes.
…headers Replace the dangerouslySetInnerHTML theme-init script in layout.tsx with a server-side cookie read so the dark class is applied on <html> before the page is sent to the browser. ThemeContext writes the theme cookie on every toggle so the server stays in sync. With no inline script remaining, next.config.mjs can declare a strict CSP with script-src 'self' and no unsafe-inline. Also adds frame-ancestors 'none', X-Frame-Options, DENY, X-Content-Type-Options, and Referrer-Policy headers. Closes Priyanshu-byte-coder#858
…etGitHubAccessToken Migrated 9 routes that still read the GitHub OAuth token from the client-exposed session object to getGitHubAccessToken(req), which reads the token from the server-side httpOnly JWT instead: - src/app/api/metrics/activity/route.ts - src/app/api/metrics/discussions/route.ts - src/app/api/metrics/inactive-repos/route.ts - src/app/api/metrics/coding-activity-insights/route.ts - src/app/api/metrics/contributions/daily/route.ts - src/app/api/metrics/contributions/hourly/route.ts - src/app/api/metrics/prs/route.ts - src/app/api/goals/sync/route.ts - src/app/api/users/search/route.ts No session.accessToken references remain in any API route.
…essToken migration session can be null when getServerSession returns without a valid session (token is authoritative now). Use optional chaining on session.gitlabToken and session.githubId/githubLogin accesses to satisfy the TypeScript strict-null check.
22ab408 to
e1f08c9
Compare
After migrating goals/sync to getGitHubAccessToken, the route now passes auth in the Playwright environment (JWT secret matches) and reaches the Supabase query. With placeholder credentials the DNS lookup for placeholder.supabase.co can stall, keeping loading=true past the 10s timeout and hiding the Goals heading. Two changes: - Add a mock for **/api/goals/sync so the POST is intercepted at the browser level, matching how all other API routes are handled - Set last_synced_at to now() on the mock goal so needsSync=false and handleSync is not triggered at all (belt-and-suspenders)
What the problem was
src/app/layout.tsxinjected a theme-initialisation<script>block viadangerouslySetInnerHTML. Because browsers block inline scripts under a Content-Security-Policy, any CSP on the site had to includescript-src 'unsafe-inline'to let this script run.'unsafe-inline'completely disables the browser's CSP protection against inline-script injection, meaning any DOM-based XSS vector — present or introduced later — executes without browser-level blocking, and can immediately call/api/auth/sessionto steal credentials. The site also had no security headers at all: no CSP, noX-Frame-Options, noX-Content-Type-Options.What was changed and in which files
src/app/layout.tsxRemoved the
dangerouslySetInnerHTMLinline script entirely. The layout now reads thethemecookie fromnext/headers(server-side, zero JS required) and appliesclassName="dark"directly on<html>before the page leaves the server. No inline script, nounsafe-inlinedependency.src/components/ThemeContext.tsxUpdated the theme provider to write a
themecookie (SameSite=Lax; path=/; max-age=1yr) viadocument.cookieon every theme change, in addition to the existinglocalStoragewrite. On mount, it reads the initial theme fromdocument.documentElement.classList(which the server already set from the cookie) to stay in sync without triggering a DOM class swap. For first-time visitors with no cookie, it falls back tolocalStoragethenprefers-color-scheme.next.config.mjsAdded a
headers()export that applies four security headers to every route:Content-Security-Policy—script-src 'self'(nounsafe-inline),style-src 'self' 'unsafe-inline'(needed for Tailwind inline styles),img-srcallows GitHub avatars,frame-ancestors 'none',base-uri 'self',form-action 'self'X-Frame-Options: DENY— belt-and-suspenders clickjacking protection alongsideframe-ancestors 'none'X-Content-Type-Options: nosniff— prevents MIME-type sniffing attacksReferrer-Policy: strict-origin-when-cross-origin— limits referrer leakageWhy this approach fixes the root cause
The root cause was the inline script itself. A nonce-based CSP would still require generating and injecting a nonce per request, adding significant complexity. The cookie approach eliminates the inline script entirely — the server reads a value it controls (the httpOnly-equivalent first-party cookie) and renders the correct class before the HTML is sent. No JavaScript runs before the page is interactive, and no CSP exception is needed. The
frame-ancestors 'none'directive replaces the previously used'self'value, since DevTrack has no legitimate embedding use case.Steps to test
npm run devand open the appContent-Security-Policycontainsscript-src 'self'with nounsafe-inlineframe-ancestors 'none'is presentX-Frame-Options: DENYis presentX-Content-Type-Options: nosniffis presentnpm run type-checkandnpm run lint— both pass with no new errorsEdge cases covered
prefers-color-schemeon mount and updates immediately; cookie is written; next load has no flashsuppressHydrationWarningon<html>is retained; server and client agree on the class because both derive it from the same cookieRegressions
None.
npm run type-checkandnpm run lintpass clean. All dashboard functionality is unaffected — the security headers apply only to the HTTP layer and the theme change is a refactor of the persistence mechanism with identical visible behaviour.Closes #858
Please review and merge this under GSSoC 2026.